﻿/******************************************************************************************************
Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************************************/

using System;
using System.Globalization;

namespace SharpGeo
{
    /// <summary>
    /// A simple lightweight implementation of ICoordinateTransformation that scales and translates IScreenCoordinates
    /// </summary>
    public class SimpleScreenCoordinateTransformation :
        ICoordinateTransformation
    {
        #region Constructors

        /// <summary>
        /// The default constructor that creates a empty SimpleScreenCoordinateTransformation
        /// </summary>
        public SimpleScreenCoordinateTransformation()
        {
        }

        /// <summary>
        /// Creates a copy of a SimpleScreenCoordinateTransformation
        /// </summary>
        /// <param name="transform">The SimpleScreenCoordinateTransformation to copy</param>
        public SimpleScreenCoordinateTransformation(SimpleScreenCoordinateTransformation transform)
        {
            if (transform == null)
                throw new ArgumentNullException("transform",
                    Properties.Resources.SimpleScreenCoordinateTransformationNullTransform);

            _transform = transform._transform;
            _inverseTransform = transform._inverseTransform;
        }

        /// <summary>
        /// Creates a SimpleScreenCoordinateTransformation with scaling and translation
        /// </summary>
        /// <param name="scaleX">The factor to scale the X coordinate</param>
        /// <param name="scaleY">The factor to scale the Y coordinate</param>
        /// <param name="translateX">The factor to translate the X coordinate</param>
        /// <param name="translateY">The factor to translate the Y coordinate</param>
        public SimpleScreenCoordinateTransformation(double scaleX, double scaleY, double translateX, double translateY)
        {
            _transform = new Matrix(scaleX, scaleY, translateX, translateY);
            _inverseTransform = _transform.CalculateInverse();
        }

        #endregion Constructors

        #region Fields

        private Matrix _transform = Matrix.Identity;
        private Matrix _inverseTransform = Matrix.Identity;

        #endregion Fields

        #region Methods

        /// <summary>
        /// Apply a scale transformation
        /// </summary>
        /// <param name="scaleX">The factor to scale the X coordinate</param>
        /// <param name="scaleY">The factor to scale the Y coordinate</param>
        public void ApplyScale(double scaleX, double scaleY)
        {
            _transform = _transform.ApplyScale(scaleX, scaleY);
            _inverseTransform = _transform.CalculateInverse();
        }

        /// <summary>
        /// Apply a translate transform
        /// </summary>
        /// <param name="deltaX">The factor to translate the X coordinate</param>
        /// <param name="deltaY">The factor to translate the Y coordinate</param>
        public void ApplyTranslate(double deltaX, double deltaY)
        {
            _transform = _transform.ApplyTranslate(deltaX, deltaY);
            _inverseTransform = _transform.CalculateInverse();
        }

        /// <summary>
        /// Apply a transformation from another SimpleScreenCoordinateTransformation
        /// </summary>
        /// <param name="transform">The SimpleScreenCoordinateTransformation to apply</param>
        public void ApplyTransform(SimpleScreenCoordinateTransformation transform)
        {
            if (transform == null)
                throw new ArgumentNullException("transform",
                    Properties.Resources.SimpleScreenCoordinateTransformationNullTransform);

            _transform *= transform._transform;
            _inverseTransform = _transform.CalculateInverse();
        }

        /// <summary>
        /// Applies the transformation to a ICoordinate
        /// </summary>
        /// <param name="coordinate">The ICoordinate to transform</param>
        /// <returns>The transformed ICoordinate</returns>
        public ICoordinate Transform(ICoordinate coordinate)
        {
            var screenCoordinate = coordinate as IScreenCoordinate;

            if (screenCoordinate != null)
            {
                return screenCoordinate * _transform;
            }

            return coordinate;
        }

        /// <summary>
        /// Applies the inverse transformation to a ICoordinate
        /// </summary>
        /// <param name="coordinate">The ICoordinate to inverse transform</param>
        /// <returns>The inversed transformed ICoordinate</returns>
        public ICoordinate InverseTransform(ICoordinate coordinate)
        {
            var screenCoordinate = coordinate as IScreenCoordinate;

            if (screenCoordinate != null)
            {
                return screenCoordinate * _inverseTransform;
            }

            return coordinate;
        }

        /// <summary>
        /// Checks to see if the passed ICoordinate can be transformed
        /// </summary>
        /// <param name="coordinate">The ICoordinate to check if it can be transformed</param>
        /// <returns>Returns true if the ICoordinate can be transformed</returns>
        public bool CanTransform(ICoordinate coordinate)
        {
            return coordinate is IScreenCoordinate;
        }

        /// <summary>
        /// Checks to see if the passed ICoordinate can be inverse transformed
        /// </summary>
        /// <param name="coordinate">The ICoordinate to check if it can be inversed transformed</param>
        /// <returns>Returns true if the ICoordinate can be inversed transformed</returns>
        public bool CanInverseTransform(ICoordinate coordinate)
        {
            return coordinate is IScreenCoordinate;
        }

        #endregion Methods

        #region Matrix

        /// <summary>
        /// A limited implementation of a matrix that for scaling and translating
        /// </summary>
        protected struct Matrix : IEquatable<Matrix>
        {
            /// <summary>
            /// Constructs a matrix
            /// </summary>
            /// <param name="scaleX">The factor to scale X by</param>
            /// <param name="scaleY">The factor to scale Y by</param>
            /// <param name="translateX">The factor to translate T by</param>
            /// <param name="translateY">The factor to translate Y by</param>
            public Matrix(
                double scaleX, double scaleY,
                double translateX, double translateY)
            {
                _scaleX = scaleX;
                _scaleY = scaleY;

                _translateX = translateX;
                _translateY = translateY;
            }

            /// <summary>
            /// Clone another Matrix
            /// </summary>
            /// <param name="matrix">The Matrix to clone</param>
            public Matrix(Matrix matrix)
            {
                _scaleX = matrix._scaleX;
                _scaleY = matrix._scaleY;

                _translateX = matrix._translateX;
                _translateY = matrix._translateY;
            }

            /// <summary>
            /// An Identity Matrix
            /// </summary>
            public static readonly Matrix Identity = new Matrix(1, 1, 0, 0);

            private double _scaleX;
            private double _scaleY;

            private double _translateX;
            private double _translateY;

            /// <summary>
            /// Applies a translate transform
            /// </summary>
            /// <param name="deltaX">The factor to translate X b</param>
            /// <param name="deltaY">The factor to translate Y by</param>
            /// <returns>A new matrix with a translate transform applied</returns>
            public Matrix ApplyTranslate(double deltaX, double deltaY)
            {
                return new Matrix(_scaleX, _scaleY, deltaX * _scaleX + _translateX, deltaY * _scaleY + _translateY);
            }

            /// <summary>
            /// Applies a scale transform
            /// </summary>
            /// <param name="scaleX">The factor to scale X by</param>
            /// <param name="scaleY">The factor to scale Y by</param>
            /// <returns>A new matrix with a scale transform applied</returns>
            public Matrix ApplyScale(double scaleX, double scaleY)
            {
                return new Matrix(scaleX * _scaleX, scaleY * _scaleY, _translateX, _translateY);
            }

            /// <summary>
            /// Returns the inverse of this matrix
            /// </summary>
            /// <returns>The inverse of this matrix</returns>
            public Matrix CalculateInverse()
            {
                var det = _scaleX * _scaleY;

                if (det == 0)
                {
                    return Matrix.Identity;
                }

                return new Matrix(1.0 / _scaleX, 1 / _scaleY, -_translateX / _scaleX, -_translateY / _scaleY);
            }

            /// <summary>
            /// Applies a matrix to a IScreenCoordinate
            /// </summary>
            /// <param name="coordinate">The IScreenCoordinate</param>
            /// <param name="matrix">The matrix</param>
            /// <returns>The transformed IScreenCoordinate</returns>
            public static IScreenCoordinate operator *(IScreenCoordinate coordinate, Matrix matrix)
            {
                if (coordinate == null || matrix == null)
                {
                    return null;
                }

                return new ScreenCoordinate(coordinate.X * matrix._scaleX + matrix._translateX, coordinate.Y * matrix._scaleY + matrix._translateY);
            }

            /// <summary>
            /// Applies a matrix to a IScreenCoordinate
            /// </summary>
            /// <param name="coordinate">The IScreenCoordinate</param>
            /// <param name="matrix">The matrix</param>
            /// <returns>The transformed IScreenCoordinate</returns>
            public static IScreenCoordinate Multiply(IScreenCoordinate coordinate, Matrix matrix)
            {
                return coordinate * matrix;
            }

            /// <summary>
            /// Multiplies two matrices together
            /// </summary>
            /// <param name="matrix1">The first matrix</param>
            /// <param name="matrix2">The second matrix</param>
            /// <returns>A new matrix representing the multiplication of the two matrices</returns>
            public static Matrix operator *(Matrix matrix1, Matrix matrix2)
            {
                return new Matrix(
                    matrix1._scaleX * matrix2._scaleX,
                    matrix1._scaleY * matrix2._scaleY,
                    matrix1._translateX + (matrix2._translateX * matrix1._scaleX),
                    matrix1._translateY + (matrix2._translateY * matrix1._scaleY));
            }

            /// <summary>
            /// Multiplies two matrices together
            /// </summary>
            /// <param name="matrix1">The first matrix</param>
            /// <param name="matrix2">The second matrix</param>
            /// <returns>A new matrix representing the multiplication of the two matrices</returns>
            public static Matrix Multiply(Matrix matrix1, Matrix matrix2)
            {
                return matrix1 * matrix2;
            }

            /// <summary>
            /// Tests this matrix with the other for equality
            /// </summary>
            /// <param name="other">The other matrix</param>
            /// <returns>True if equal</returns>
            public bool Equals(Matrix other)
            {
                return
                    _scaleX == other._scaleX && _scaleY == other._scaleY &&
                    _translateX == other._translateX && _translateY == other._translateY;
            }

            /// <summary>
            /// Tests this matrix with obj for equality
            /// </summary>
            /// <param name="obj">The obj to test for equality</param>
            /// <returns>True if equal</returns>
            public override bool Equals(object obj)
            {
                return (obj is Matrix) ? Equals((Matrix)obj) : false;
            }

            /// <summary>
            /// Tests two matrices for equality
            /// </summary>
            /// <param name="matrix1">The first matrix</param>
            /// <param name="matrix2">The second matrix</param>
            /// <returns>True if equal</returns>
            public static bool operator ==(Matrix matrix1, Matrix matrix2)
            {
                return matrix1.Equals(matrix2);
            }

            /// <summary>
            /// Tests two matrices for inequality
            /// </summary>
            /// <param name="matrix1">The first matrix</param>
            /// <param name="matrix2">The second matrix</param>
            /// <returns>True if inequal</returns>
            public static bool operator !=(Matrix matrix1, Matrix matrix2)
            {
                return !(matrix1.Equals(matrix2));
            }

            /// <summary>
            /// Gets the computed hashcode of this matrix
            /// </summary>
            /// <returns>The computed hashcode</returns>
            public override int GetHashCode()
            {
                unchecked
                {
                    long l =
                        BitConverter.DoubleToInt64Bits(_scaleX) +
                        (37 * BitConverter.DoubleToInt64Bits(_scaleY)) +
                        (43 * BitConverter.DoubleToInt64Bits(_translateX)) +
                        (47 * BitConverter.DoubleToInt64Bits(_translateY));

                    return (int)((l >> 32) ^ l);
                }
            }

            /// <summary>
            /// Converts this matrix to a string
            /// </summary>
            /// <returns>The converted string</returns>
            public override string ToString()
            {
                return string.Format(
                    CultureInfo.InvariantCulture,
                    "(({0},0,{1}), (0,{2},{3}), (0,0,1))",
                    _scaleX, _translateX,
                    _scaleY, _translateY);
            }
        }

        #endregion Matrix
    }
}